我們的 Particle System 會更新我們 Particle 的所以物理,以及幫助我們將粒子繪製出來。
class ParticleSystem {
public:
ParticleSystem() = default;
void update(entt::registry ®istry, float dt);
void render(entt::registry ®istry);
void init(std::shared_ptr<Shader> shader);
void initEmitter(entt::registry ®istry, entt::entity &entity, EmitData data);
private:
std::shared_ptr<Shader> shader;
std::shared_ptr<VertexArray> vertexArray;
uint32_t vao;
}
所以我們的 ParticleSystem 會有基本的一些更新及渲染的方法。所以這裡存著我封裝的 Shader 以及我封裝的 vertexArray。
在遊戲進入循環之前,我們要先將我們的 Particle System 選染所要用的東西都先初始化好不然會出事。
void ParticleSystem::init(std::shared_ptr<Shader> shader) {
this->shader = shader;
GLfloat particle_quad[] = {
0.0f, 1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f, 0.0f
};
vertexArray = std::make_shared<VertexArray>();
vertexArray->bind();
std::shared_ptr<VertexBuffer> vertexBuffer;
vertexBuffer = std::make_shared<VertexBuffer>(particle_quad, sizeof(particle_quad));
BufferLayout layout = {
{ShaderDataType::Float4, "vertex"}
};
vertexBuffer->setLayout(layout);
vertexArray->addVertexBuffer(vertexBuffer);
vertexArray->unbind();
}
進入遊戲循環之後呢,我們會按下鍵盤上的按鍵,隨後從 Json 檔案裡面讀入資料並輸入給 Emitter。Particle System 負責幫助輸入資料。
void ParticleSystem::initEmitter(entt::registry ®istry, entt::entity &entity, EmitData data) {
auto &emitter = registry.get<EmitterComponent>(entity);
auto &transform = registry.get<TransformComponent>(entity);
auto &data = emitter.data;
// 將讀進來的設定檔案全部設置到emitter
emitter.angleRange = data.angleRange;
emitter.startSpeed = data.startSpeed;
emitter.endSpeed = data.endSpeed;
emitter.startSize = data.startSize;
emitter.endSize = data.endSize;
emitter.rotateSpeed = data.rotateSpeed;
emitter.emitNumber = data.emitNumber;
emitter.emitVariance = data.emitVariance;
emitter.maxParticleLife = data.maxParticleLife;
emitter.maxParticlesPerFrame = data.emitNumber + data.emitVariance;
emitter.poolSize = emitter.maxParticlesPerFrame * (emitter.maxParticleLife + 1);
emitter.particles.resize(emitter.poolSize);
// Render Properties
emitter.startColor = data.startColor;
emitter.endColor = data.endColor;
emitter.active = true;
emitter.lifeTime = emitter.life = data.life;
emitter.sleepTime = data.sleepTime;
emitter.rotSpeedRand = data.rotSpeedRand;
emitter.startSpeedRand = data.startSpeedRand;
emitter.endSpeedRand = data.endSpeedRand;
emitter.emitVarianceRand = data.emitVarianceRand;
emitter.startSizeRand = data.startSizeRand;
emitter.endSizeRand = data.endSizeRand;
emitter.lifeRand = data.lifeRand;
emitter.disX = data.disX;
emitter.disY = data.disY;
}
就是字面上把所有的資料給 Emitter。
有資料的 Emitter 就要開始模擬效果了。
void ParticleSystem::update(entt::registry ®istry, float dt) {
auto group = registry.view<ParticleComponent, TransformComponent>();
for( auto entity : group){
auto &emitter = registry.get<ParticleComponent>(entity);
auto &transform = registry.get<TransformComponent>(entity);
if (emitter.active) {
// cal the num that should respawn this frame
emitter.emissionRate = (int)(emitter.emitNumber + emitter.emitVariance * randFloat(emitter.emitVarianceRand.x, emitter.emitVarianceRand.y));
for(int i = 0 ; i < emitter.emissionRate ; i++) {
// find the unused Particle
int unusedParticle = firstUnusedParticle(registry, entity);
// 激活他
respawnParticle(emitter, transform, unusedParticle);
}
}
}
}
首先我們找出所有有 ParticleComponent 的 Entity,並更新他們。
更新的時候,首先確認此 Emitter 是否活著(emitter.active
)如果活著的話,計算出這一個 frame 要產生多少個 particle(emitter.emissionRate
),然後激活emissionRate
數量的粒子。
激活粒子方法呢,我們會在我們的 Particle pool 中,找到未被使用的粒子並將它激活。
uint32_t ParticleSystem::firstUnusedParticle(entt::registry ®istry, entt::entity &entity) {
auto &particle = registry.get<EmitterComponent>(entity);
for(int i = particle.lastUnusedParticle ; i < particle.poolSize ; i++) {
if(particle.particles[i].life <= 0) {
particle.lastUnusedParticle = i;
return i;
}
}
for(int i = 0 ; i < particle.lastUnusedParticle ; i++) {
if(particle.particles[i].life <= 0) {
particle.lastUnusedParticle = i;
return i;
}
}
particle.lastUnusedParticle = 0;
return particle.lastUnusedParticle;
}
激活就是將 Emitter 的數據給每顆 Particle
void EmitterSystem::respawnParticle(EmitterComponent& emitter, TransformComponent& transform, int unusedParticle) {
// 用random計算讓particle看起來不會太整齊
float tmpStartSpeed = emitter.startSpeed * randFloat(emitter.startSpeedRand.x, emitter.startSpeedRand.y);
float tmpEndSpeed = emitter.endSpeed * randFloat(emitter.endSpeedRand.x, emitter.endSpeedRand.y);
float randAngle = randFloat(emitter.angleRange.x, emitter.angleRange.y);
// random size
float randStart = emitter.startSize * randFloat(emitter.startSizeRand.x, emitter.startSizeRand.y);
float randEnd = emitter.endSize * randFloat(emitter.endSizeRand.x, emitter.endSizeRand.y);
float randRadius = randFloat(randStart, randEnd/2);
float randlife = emitter.maxParticleLife * randFloat(emitter.lifeRand.x, emitter.lifeRand.y);
float randRotSpeed = emitter.rotateSpeed * randFloat(emitter.rotSpeedRand.x, emitter.rotSpeedRand.y);
float randRotAngle = randFloat(0.f , emitter.rotateSpeed == 0 ? 0.f : 360.f);
p2 dis = {randFloat(emitter.disX, -emitter.disX), randFloat(emitter.disY, -emitter.disY)};
// Respawn Particle
emitter.particles[unusedParticle].pos = transform.pos + dis; // depends on emitter's pos
emitter.particles[unusedParticle].startVel.x = tmpStartSpeed * cos(DEG_2_RAD(randAngle));
emitter.particles[unusedParticle].startVel.y = tmpStartSpeed * sin(DEG_2_RAD(randAngle));
emitter.particles[unusedParticle].endVel.x = tmpEndSpeed * cos(DEG_2_RAD(randAngle));
emitter.particles[unusedParticle].endVel.y = tmpEndSpeed * sin(DEG_2_RAD(randAngle));
emitter.particles[unusedParticle].startRotSpeed = randRotSpeed;
emitter.particles[unusedParticle].currentRotSpeed = randRotSpeed;
emitter.particles[unusedParticle].angle = randRotAngle; // texture rotate
// Life Properties
emitter.particles[unusedParticle].life = emitter.particles[unusedParticle].startLife = randlife;
emitter.particles[unusedParticle].currentSize = emitter.particles[unusedParticle].startSize = randRadius;
emitter.particles[unusedParticle].endSize = emitter.endSize;
emitter.particles[unusedParticle].t = 0.f;
// Color Properties
emitter.particles[unusedParticle].startColor = emitter.startColor;
emitter.particles[unusedParticle].endColor = emitter.endColor;
}
如果有設定 sleep time 或是 emitter life time 記得隨時開關 emitter.active
for( auto entity : group){
//[...]
if(emitter.sleepTime > 0.f) {
if(emitter.sleepTimer.read() <= emitter.sleepTime){
emitter.active = false;
}
else {
emitter.active = true;
emitter.sleepTimer.start();
}
}
// if emitter still has life
if(emitter.life > 0.f) {
// exceeded
if(emitter.lifeTimer.read() >= emitter.life) {
emitter.active = false;
}
}
}
接著有了數據的 Particle 就可以來更新了。
for( auto entity : group) {
//[...]
for(int i = 0 ; i < emitter.poolSize ; i++) {
auto &particle = emitter.particles[i];
if(particle.life > 0.f) {
// Update
// Interpolate values
// 利用差質法算出目前的資訊
particle.currentSize = interpolateBetweenRange(particle.startSize, particle.t, particle.endSize);
particle.currentVel.x = interpolateBetweenRange(particle.startVel.x, particle.t, particle.endVel.x);
particle.currentVel.y = interpolateBetweenRange(particle.startVel.y, particle.t, particle.endVel.y);
particle.currentColor = RGBAinterpolation(particle.startColor, particle.t, particle.endColor);
particle.pos.x += particle.currentVel.x * dt;
particle.pos.y += particle.currentVel.y * dt;
particle.life--;
// 計算差值的time
particle.t += (1.0f/(float)particle.startLife);
if(particle.t >= 1.f)
particle.t = 0.f;
//
// 計算rotate速度跟角度
particle.currentRotSpeed += particle.startRotSpeed;
particle.angle += dt * particle.currentRotSpeed;
particle.angle = fmod(particle.angle, 360.f);
}
}
}
如果那顆粒子是活的,我們就更新他。
利用插值法更新速度、大小以及顏色。
然後根據速度以及這一個 frame 經過的時間更新位置
計算其他東西。
差值法
float ParticleSystem::interpolateBetweenRange(float min, float timestep, float max) {
return min + (max - min) * timestep;
}
p4 ParticleSystem::RGBAinterpolation(p4 startColor, float timestep, p4 endColor) {
p4 finColor;
finColor.r = startColor.r + (endColor.r - startColor.r) * timestep;
finColor.g = startColor.g + (endColor.g - startColor.g) * timestep;
finColor.b = startColor.b + (endColor.b - startColor.b) * timestep;
finColor.a = startColor.a + (endColor.a - startColor.a) * timestep;
return finColor;
}
這樣就完成更新啦
明天就來畫出來吧!